/*  Copyright (C) 2008  Alexander Blyzniuchenko (BlezAlex)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Text.RegularExpressions;
using System.Xml;
using System.Text;

namespace DC.Http
{
    public class WebServer
    {
        private readonly HttpListener _listener;
        private readonly Dictionary<string, long> _fileLengthCollection;
        private static readonly List<RequestMethod> _NonWebDavMethods = new List<RequestMethod>(new RequestMethod[] { RequestMethod.GET });
        private static readonly Regex _rangeRegEx = new Regex(@"bytes\s*=\s*(?<start>\d+)\s*-(\s*(?<end>\d+))*", RegexOptions.IgnoreCase | RegexOptions.Compiled);
        private string _pref;

        public WebServer()
        {
            _listener = new HttpListener();
            _fileLengthCollection = new Dictionary<string, long>();
        }

        public void Start(int port)
        {          
            _listener.Start(IPAddress.Any, port);

            _pref = port != 80 ? string.Format("http://localhost:{0}", port) : "http://localhost";

            _listener.OnClientConnected += OnClientConnected;
            Trace.WriteLine("Listening");
        }

        public void Stop()
        {
            _listener.Stop();
        }

        private static int GetConnectionID(string url)
        {
            url = url.Substring(1);
            string strNumber = url.Substring(0, url.IndexOf("/"));
            return int.Parse(strNumber);
        }

        private static string GetFileUrl(string url)
        {
            url = url.Substring(1);
            return url.Substring(url.IndexOf("/") + 1);
        }

        private void OnClientConnected(object sender, ClientRecivedEventArgs e)
        {
            Request request = e.Request;
            Response response = e.Response;

            response.Headers.Add("Accept-Ranges", "bytes");

            if (_NonWebDavMethods.Contains(request.Method))
            {
                GetMethodProcessing(request, response);
            }
            else
            {
                Trace.WriteLine(String.Format("WEBDAV {0} {1}", request.Method, request.Url));
                if (request.Method == RequestMethod.OPTIONS)
                {
                    OptionsMethodProcessing(response);
                }
                else if (request.Method == RequestMethod.PROPFIND)
                {
                    PropFindMethodProcessing(request, response);
                }
                else
                    Trace.WriteLine("UNKNOW METHOD!!" + request.Method);
            }

        }

        private void PropFindMethodProcessing(Request request, Response response)
        {
            try
            {
                Trace.WriteLine("WebDAV Request PROPFIND");
                List<Property> properties = null;

                if (request.Headers["Content-Length"] != "0")
                {
                    properties = GetPropfindRequestedProperties(request);
                }

                int depth = string.IsNullOrEmpty(request.Headers["Depth"]) ? 1 : int.Parse(request.Headers["Depth"]);

                XmlDocument reply = new XmlDocument();
                reply.Prefix = "a";
                XmlElement multistatusElement = reply.CreateElement("a","multistatus", "DAV:");
                reply.AppendChild(multistatusElement);

                BuildLevel(properties, reply, multistatusElement, HttpUtility.GetRelaivePath(request.Url), depth);

                response.StatusCode = 207;
                response.StatusDescription = "Multi-Status";
                response.Headers.Add("Content-Type", "text/xml");

                using (XmlWriter xw = XmlWriter.Create(response))
                    reply.WriteContentTo(xw);

                StringBuilder sb = new StringBuilder();
                using (XmlWriter xw = XmlWriter.Create(sb))
                    reply.WriteContentTo(xw);
                Debug.WriteLine(sb.ToString());

                response.Close();
            }
            catch (IOException)
            {
                Trace.WriteLine("HTTP Client D I S C O N N E C T E D !");
            }
            catch (Exception ex)
            {
                response.StatusCode = 500;
                response.StatusDescription = "Internal Server Error";
                response.Close();
                Trace.WriteLine(ex);
            }
        }

        private static List<Property> GetPropfindRequestedProperties(Request request)
        {
            List<Property> properties;
            byte[] data = new byte[int.Parse(request.Headers["Content-Length"])];
            request.InputStream.Read(data, 0, data.Length);
            string str = Encoding.GetEncoding(1251).GetString(data);
                    
            XmlDocument xdoc = new XmlDocument();
            xdoc.Load(new StringReader(str));

            XmlNodeList propNodes = xdoc.GetElementsByTagName("prop", "DAV:");
            if (propNodes.Count != 1)
                throw new InvalidOperationException("prop element not found or more then one");

            XmlNode propNode = propNodes[0];
            properties = new List<Property>(propNode.ChildNodes.Count);
            foreach (XmlNode node in propNode.ChildNodes)
                properties.Add(Property.Get(node.LocalName, node.NamespaceURI));
            return properties;
        }

        private static void OptionsMethodProcessing(Response response)
        {
            response.StatusCode = 200;
            response.ContentLength64 = 0;
            response.StatusDescription = "OK";
            response.Headers.Add("DAV", "1, 2");
            response.Headers.Add("Public", "OPTIONS, TRACE, GET, HEAD, DELETE, PUT, POST, COPY, MOVE, MKCOL, PROPFIND, PROPPATCH, LOCK, UNLOCK, SEARCH");
            response.Headers.Add("Allow", "OPTIONS, TRACE, GET, HEAD, DELETE, COPY, MOVE, PROPFIND, PROPPATCH, SEARCH, MKCOL, LOCK, UNLOCK");

            response.Close();
        }

        private void BuildLevel(IEnumerable<Property> properties, XmlDocument reply, XmlNode multistatusElement, string path, int depth)
        {
            DcDirectory thisElement = HubConnectionManager.DcRoot[path];

            // A file. NOT a dir or not existing dir
            if (thisElement == null)
            {
                DcDirectory parrentDir = HubConnectionManager.DcRoot[HttpUtility.GetPath(path)];
                if (parrentDir == null)
                    return;

                string targetFileName = HttpUtility.GetFileName(path);

                DcFile file = parrentDir.Files.Find(delegate(DcFile fileToCheck) { return fileToCheck.Name == targetFileName; });
                if (file.Name == null) // file not found
                    return;
                
                RenderElement(HttpUtility.ConcatPath(path, file.Name), reply, multistatusElement, new ResponseElement(file), properties);
                return;
            }
            else
            {
                RenderElement(path, reply, multistatusElement, new ResponseElement(thisElement), properties);

                if (depth > 0)
                {
                    foreach (DcDirectory directory in thisElement.Directories)
                    {
                        BuildLevel(properties, reply, multistatusElement, HttpUtility.ConcatPath(path, directory.Name), depth - 1);
                    }

                    foreach (DcFile file in thisElement.Files)
                        RenderElement(HttpUtility.ConcatPath(path, file.Name), reply, multistatusElement, new ResponseElement(file), properties); 
                }
            }
        }

        private void RenderElement(string path, XmlDocument reply, XmlNode multistatusElement, ResponseElement element, IEnumerable<Property> properties)
        {
            XmlElement responseElement = reply.CreateElement("a", "response", "DAV:");
            multistatusElement.AppendChild(responseElement);

            XmlElement hrefElement = reply.CreateElement("a", "href", "DAV:");
            responseElement.AppendChild(hrefElement);
            hrefElement.InnerText = _pref + path;

            XmlElement propstatFoundElement = reply.CreateElement("a", "propstat", "DAV:");
            responseElement.AppendChild(propstatFoundElement);
            XmlElement statusFoundElement = reply.CreateElement("a", "status", "DAV:");
            propstatFoundElement.AppendChild(statusFoundElement);
            statusFoundElement.InnerText = "HTTP/1.1 200 OK";
            XmlElement propFoundElement = reply.CreateElement("a", "prop", "DAV:");
            propstatFoundElement.AppendChild(propFoundElement);

            if (properties != null)
            {
                XmlElement propstatNotFoundElement = reply.CreateElement("a", "propstat", "DAV:");
                responseElement.AppendChild(propstatNotFoundElement);
                XmlElement statusNotFoundElement = reply.CreateElement("a", "status", "DAV:");
                statusNotFoundElement.InnerText = "HTTP/1.1 404 Resource Not Found";
                propstatNotFoundElement.AppendChild(statusNotFoundElement);
                XmlElement propNotFoundElement = reply.CreateElement("a", "prop", "DAV:");
                propstatNotFoundElement.AppendChild(propNotFoundElement);

                foreach (Property p in properties)
                {
                    XmlNode property = reply.CreateElement("a", p.Name, p.Namespace);
                    if (element.Properties.ContainsKey(p))
                    {
                        property.InnerText = element.Properties[p];
                        propFoundElement.AppendChild(property);
                    }
                    else
                    {
                        propNotFoundElement.AppendChild(property);
                    }
                }
            }
            else
            {
                foreach (Property p in element.Properties.Keys)
                {
                    XmlElement property = reply.CreateElement("a", p.Name, p.Namespace);
                    property.InnerText = element.Properties[p];
                    propFoundElement.AppendChild(property);
                }
            }
        }


        private void GetMethodProcessing(Request request, Response response)
        {
            long startPos = 0;
            long endPos = -1;
            Task t = new Task();

            try
            {
                string ranges = null;
                if (request.Headers.ContainsKey("Range"))
                    ranges = request.Headers["Range"];

                Trace.WriteLine(String.Format("{0} {1}", request.Url, ranges));


                string nick;
                string filePath;
                string fileURL;
                HubClient hubConnection;

                try
                {
                    fileURL = GetFileUrl(request.Url);

                    nick = fileURL.Substring(0, fileURL.IndexOf("/"));
                    filePath = fileURL.Substring(fileURL.IndexOf("/") + 1); 
                    hubConnection = HubConnectionManager.GetConnectedClient(GetConnectionID(request.Url));
                }
                catch(FormatException)
                {
                    string path = HttpUtility.GetRelaivePath(request.Url);
                    DcDirectory parrentDir = HubConnectionManager.DcRoot[HttpUtility.GetPath(path)];
                    if (parrentDir == null)
                        return;

                    string targetFileName = HttpUtility.GetFileName(path);

                    DcFile file = parrentDir.Files.Find(delegate(DcFile fileToCheck) { return fileToCheck.Name == targetFileName; });

                    string tmp = HttpUtility.GetPath(path).Substring(1);
                    int firstSlash = tmp.IndexOf("/");
                    string serv = tmp.Substring(0, firstSlash);
                    int secondSlash = tmp.IndexOf("/", firstSlash + 1);
                    nick = tmp.Substring(firstSlash + 1, secondSlash - firstSlash - 1);

                    fileURL = string.Format("{0}/TTH/{1}", nick, file.TTH);
                    filePath = path;

                    hubConnection = HubConnectionManager.GetConnectedClient(serv);
                }

                
                if (hubConnection == null)
                    throw new FileNotFoundException("Connection not found");

                if (!string.IsNullOrEmpty(ranges))
                {
                    Match m = _rangeRegEx.Match(ranges);
                    if (m.Success)
                    {
                        startPos = long.Parse(m.Groups["start"].Value);
                        string endPosStr = m.Groups["end"].Value;
                        if (!string.IsNullOrEmpty(endPosStr))
                            endPos = long.Parse(endPosStr);
                    }
                }

                t = TaskMonitor.StartTask(nick, filePath);

                DCStream source = null;

                try
                {
                    if (startPos > 0)
                    {
                        long fileLen = _fileLengthCollection[fileURL];
                        if (endPos == -1)
                            endPos = fileLen;

                        Debug.Assert(endPos > startPos);
                        source = hubConnection.GetFile(fileURL, startPos, endPos - startPos);

                        response.StatusCode = 206;
                        response.StatusDescription = "Partial Content";
                        response.Headers.Add("Content-Range", string.Format("bytes {0}-{1}/{2}", startPos, endPos, fileLen));
                    }
                    else
                    {
                        source = hubConnection.GetFile(fileURL, 0, -1);
                        _fileLengthCollection[fileURL] = source.Length;
                        endPos = source.Length;
                    }
                    t.Status = TaskStatus.Downloading;
                    t.Total = _fileLengthCollection[fileURL];
                    t.Position = startPos;

                    response.ContentLength64 = endPos - startPos;
                    StreamProcessor.Stream2Stream(source, response, endPos - startPos, t);
                    response.Close();

                }
                finally
                {
                    if (source != null)
                        source.Dispose();
                    t.Status = TaskStatus.Completed;
                }
            }
            catch (FileNotFoundException ex)
            {
                response.StatusCode = 404;
                response.StatusDescription = ex.Message;
                response.Close();
                Debug.WriteLine("FileNotFoundException: {0}", ex.Message);
                t.Status = TaskStatus.Failed;
                t.Text = ex.Message;
            }
            catch (IOException)
            {
                Debug.WriteLine("HTTP Client D I S C O N N E C T E D !");
                t.Status = TaskStatus.Canceled;
                t.Text = "HTTP Client DISCONNECTED";
            }
            catch (TimeoutException ex)
            {
                Debug.WriteLine("TIMEOUT: {0}", ex.Message);
                response.StatusCode = 404;
                response.StatusDescription = ex.Message;
                response.Close();
                t.Status = TaskStatus.Failed;
                t.Text = "TIMEOUT: " + ex.Message;
            }
            catch (Exception ex)
            {
                response.StatusCode = 500;
                response.StatusDescription = "Internal Server Error";
                t.Status = TaskStatus.Failed;
                t.Text = ex.Message;
                Debug.WriteLine(ex);
            }
        }
    }
}